<?php
/**
 * Copyright (c) 2006-2008, Julien PORTALIER
 * http://featherphp.googlecode.com/
 * 
 * Originally derived from CakePHP,
 * Copyright (c) 2006, Cake Software Foundation, Inc.
 * 
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 */

/**
 * Manipulates HTML form elements.
 */
class form
{
	static private $objects          = array();
	static private $js_scripts       = array();
	static private $js_scripts_added = array();
	static private $js_inline        = array();
	
	/**
	 * Binds an Object.
	 * 
	 * @param $model  String
	 * @param $object Object
	 */
	static function bind($model, Object $object)
	{
		self::$objects[$model] = $object;
	}
	
	/**
	 * Begins a form.
	 * 
	 * @param $action String
	 * @param $method String[optional]
	 * @param $attr   Array[optional]
	 */
	static function start($action, $method='post', array $attr=null)
	{
		self::$js_scripts = array();
		self::$js_inline  = array();
		
		# starts form
		$attr['action'] = html::url($action);
		$attr['method'] = strtolower($method);
		return html::start_tag('form', &$attr);
	}
	
	/**
	 * Ends a form.
	 */
	static function end()
	{
		# adds required javascript
		$script = '';
		if (!empty(self::$js_inline))
		{
			foreach(self::$js_scripts as $js_script)
				$script .= html::js($js_script);
			
			self::$js_scripts_added = array_merge(self::$js_scripts_added, self::$js_scripts);
			self::$js_scripts = array();
		}
		if (!empty(self::$js_inline))
		{
			$script .= html::js_script(implode("\n", self::$js_inline));
			self::$js_inline  = array();
		}
		
		# ends form
		return $script.html::end_tag('form');
	}
	
	/**
	 * Adds a javascript file required by the form.
	 * 
	 * @param $url String
	 */
	static function add_js_script($url)
	{
		if (!in_array($url, self::$js_scripts_added)
			and !in_array($url, self::$js_scripts))
		{
			self::$js_scripts[] = $url;
		}
	}
	
	/**
	 * Adds some inline javascript to the form.
	 * 
	 * @param $script String
	 */
	static function add_js_inline($script)
	{
		self::$js_inline[] = $script;
	}
	
	
	// TAGS
	
	/**
	 * 
	 * @return String
	 * @param $title String
	 * @param $field String[optional]
	 * @param $attr  Array[optional]
	 */
	static function label_tag($title, $field=null, array $attr=null)
	{
		if (is_array($field) and empty($attr))
			$attr =& $field;
		if (empty($attr['for']))
			$attr['for'] = self::field_to_id($field);
		return html::content_tag('label', $title, &$attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $type  String
	 * @param $attr  Array[optional]
	 */
	static function input_tag($field, $type, array $attr=null)
	{
		$attr['type']  = $type;
		$attr['name']  = self::field_to_name($field);
		$attr['class'] = (isset($attr['class']) ? $attr['class'].' ' : '').$type;
		
		if (!isset($attr['id'])) {
			$attr['id'] = self::field_to_id($field);
		}
		if (isset($attr['_error']))
		{
			$_error = $attr['_error'];
			unset($attr['_error']);
		}
		
		switch ($type)
		{
			case 'text':
			case 'password':
				$attr['value'] = self::field_value($field, &$attr);
				
				# is there a model for field?
				if (strpos($field, '.'))
				{
					list($model, $_field) = explode('.', $field, 2);
					
					# tries to load a generic model, if one wasn't binded manually
					if (!isset(self::$objects[$model]))
					{
						if (class_exists($model, true)) {
							self::$objects[$model] = new $model();
						}
					}
					
					# are there informations about the field?
					if (isset(self::$objects[$model], self::$objects[$model]->_fields[$_field]))
					{
						$_field = self::$objects[$model]->_fields[$_field];
						
						# length
						if (isset($_field['length'])) {
							$attr['maxlength'] = $_field['length'];
						}
						
						# special type?
						switch($_field['type'])
						{
							case 'datetime': $attr['class'] .= ' datetime'; break;
							case 'date':     $attr['class'] .= ' date';     break;
							case 'time':     $attr['class'] .= ' time';     break;
						}
					}
				}
				break;
			
			case 'hidden':
				$attr['value'] = self::field_value($field, &$attr);
				$_error = false;
				break;
			
			case 'checkbox':
				if (isset($attr['checked']) and $attr['checked'] === false) {
					unset($attr['checked']);
				}
				elseif (self::field_value($field, &$attr)) {
					$attr['checked'] = 'checked';
				}
				$_error = false;
				break;
			
			case 'radio':
				if (isset($attr['value']) and self::field_value($field) == $attr['value']) {
					$attr['checked'] = 'checked';
				}
				break;
			
			case 'file':
				break;
			
			default:
				trigger_error("Unknown input type: $type.", E_USER_WARNING);
		}
		
		$str  = html::tag('input', &$attr);
		if (!isset($_error) or $_error) {
			$str .= self::field_error_message($field);
		}
		return $str;
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $value Mixed[optional]
	 * @param $attr  Array[optional]
	 */
	static function hidden_field($field, $value=null, array $attr=null)
	{
		if ($value !== null and !isset($attr['value']))
			$attr['value'] = $value;
		return self::input_tag($field, 'hidden', $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr  Array[optional]
	 */
	static function text_field($field, array $attr=null)
	{
		if ((isset($attr['autocomplete']) and $attr['autocomplete'] != 'off')
			or isset($attr['autocomplete.multi'])
		)
		{
			if (isset($attr['autocomplete.multi']))
			{
				$type = 'Multi';
				$url  = $attr['autocomplete.multi'];
				unset($attr['autocomplete.multi']);
			}
			else
			{
				$type = 'Base';
				$url  = $attr['autocomplete'];
			}
			
			$attr['autocomplete'] = 'off';
			$id = self::field_to_id($field);
			
			self::add_js_script('ui/form/autocompleter.js');
			self::add_js_inline("new Autocompleter.$type('$id', '".html::url($url)."');");
		}
		return self::input_tag($field, 'text', $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr  Array[optional]
	 */
	static function password_field($field, array $attr=null)
	{
		$attr['class'] = 'text';
		return self::input_tag($field, 'password', $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr  Array[optional]
	 */
	static function file_field($field, array $attr=null)
	{
		return self::input_tag($field, 'file', $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr  Array[optional]
	 */
	static function text_area($field, array $attr=null)
	{
		$attr['name'] = self::field_to_name($field);
		$value = self::field_value($field, &$attr);
		
		if (empty($attr['id']))
			$attr['id'] = self::field_to_id($field);
		
		unset($attr['value']);
		$str  = html::content_tag('textarea', $value, $attr);
		$str .= self::field_error_message($field);
		return $str;
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr  Array[optional]
	 */
	static function check_box($field, array $attr=null)
	{
		return self::input_tag($field, 'checkbox', $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr  Array[optional]
	 */
	static function radio_button($field, array $attr=null)
	{
		return self::input_tag($field, 'radio', $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field     String
	 * @param $radios    Array A list of radio buttons.
	 * @param $attr      Array[optional]
	 * @param $separator String[optional]
	 */
	static function radio_buttons($field, array $radios, array $attr=null, $separator=" ")
	{
		$id      = self::field_to_id($field);
		$content = '';
		$index   = 0;
		
		foreach($radios as $value => $label)
		{
			$r_attr = $attr;
			$r_attr['_error'] = false;
			$r_attr['id']     = $id.'_'.$index;
			$r_attr['value']  = $value;
			
			$content .= self::radio_button($field, &$r_attr);
			$content .= self::label_tag($label, $r_attr['id']);
			$content .= $separator;
			
			$index++;
		}
		
		$content .= self::field_error_message($field);
		return $content;
	}
	
	/**
	 * 
	 * @return String
	 * @param $field   String
	 * @param $options Array A list of select options.
	 * @param $attr    Array[optional]
	 */
	static function select($field, array $options, array $attr=null)
	{
		$selected = self::field_value($field, $attr);
		$content  = self::select_options(&$options, $selected);
		
		if (empty($attr['id']))
			$attr['id'] = self::field_to_id($field);
		$attr['name'] = self::field_to_name($field);
		
		unset($attr['value']);
		$str  = html::content_tag('select', $content, $attr);
		$str .= self::field_error_message($field);
		return $str;
	}
	
	/**
	 * 
	 * @return String
	 * @param $field   String
	 * @param $options Array A list of select options.
	 * @param $attr    Array[optional]
	 */
	static function select_many($field=null, array $options, array $attr=null)
	{
		$attr['multiple'] = 'multiple';
		return self::select($field, &$options, $attr);
	}
	
	/**
	 * 
	 * @return String
	 * @param $options  Array A list of options.
	 * @param $selected Mixed The selected option.
	 */
	static function select_options(array $options, $selected=null)
	{
		$content = '';
		foreach($options as $value => $name)
		{
			$_attr = array('value' => $value);
			if ($value == $selected)
				$_attr['selected'] = 'selected';
			$content .= html::content_tag('option', $name, &$_attr);
		}
		return $content;
	}
	
	/**
	 * 
	 * @return 
	 * @param $title String
	 * @param $name  String[optional]
	 * @param $attr  Array[optional]
	 */
	static function submit_tag($title, $name=null, array $attr=null)
	{
		$attr['value'] = $title;
		$attr['type']  = 'submit';
		if (!empty($name))
			$attr['name']  = $name;
		return html::tag('input', &$attr);
	}
	
	
	// VALIDATION ERRORS
	
	/**
	 * 
	 * @return String
	 * @param $mesg    String[optional]
	 * @param $objects Mixed[optional] Object or Array of Object.
	 * @param $display_all Boolean[optional] Display all errors or just the first one?
	 */
	static function all_error_messages($mesg=null, $objects=null, $display_all=false)
	{
		if (empty($objects))
		{
			$objects =& self::$objects;
		}
		elseif ($objects instanceof Object)
		{
			$objects = array($objects);
		}
		elseif (is_string($objects) and isset(self::$objects[$objects]))
		{
			$objects = array(self::$objects[$objects]);
		}
		
		# no objects -> no error to display
		if (empty($objects))
			return '';
		
		# gets errors for each object
		$list = array();
		foreach($objects as $obj)
		{
			if (isset($obj, $obj->_errors) and !$obj->_errors->is_empty())
			{
				foreach($obj->_errors as $err)
				{
					foreach($err as $err_str)
					{
						$list[] = "<li>$err_str</li>";
						if (!$display_all)
						{
							break;
						}
					}
				}
			}
		}
		
		# no errors
		if (empty($list))
		{
			return '';
		}
		
		# render
		$str  = '<div class="errors">';
		$str .= '<p>'.(empty($mesg) ? t('Please fix the following fields:') : $mesg).'</p>';
		$str .= '<ul class="errors">'.implode($list).'</ul>';
		$str .= '</div>';
		return $str;
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $display_all Boolean[optional] Display all errors or just the first one?
	 */
	static function field_error_message($field, $display_all=false)
	{
		if (strpos($field, '.'))
		{
			list($model, $field) = explode('.', $field, 2);
			if (isset(self::$objects[$model]))
			{
				$object = self::$objects[$model];
				
				# any error(s) to report?
				if (isset($object->_errors) and !$object->_errors->is_valid($field))
				{
					$err = $object->_errors->on($field);
					
					if (is_array($err))
					{
						$str = '';
						foreach($err as $err_str)
						{
							$str .= ' <span class="error">'.$err_str.'</span> ';
							if (!$display_all)
								break;
						}
						return $str;
					}
					else
					{
						return ' <span class="error">'.$err.'</span> ';
					}
				}
			}
		}
		return '';
	}
	
	
	// FIELDS & DATA MANIPULATION
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 * @param $attr Array[optional]
	 */
	static function field_value($field, array $attr=null)
	{
		$value = null;
		
		if (!isset($attr['value']))
		{
			if (strpos($field, '.'))
			{
				list($model, $field) = explode('.', $field, 2);
				
				if (isset(self::$objects[$model], self::$objects[$model]->$field)) {
					$value = self::$objects[$model]->$field;
				}
				elseif (isset($_REQUEST[$model][$field])) {
					$value = $_REQUEST[$model][$field];
				}
			}
			elseif (isset($_REQUEST[$field])) {
				$value = $_REQUEST[$field];
			}
		}
		else {
			$value = $attr['value'];
		}
		return htmlspecialchars($value);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 */
	static function field_to_id($field)
	{
		return strpos($field, '.') ?
			Inflector::variablize(str_replace('.', '_', $field)) :
			preg_replace('[^0-9a-zA-Z\-\_]', '', $field);
	}
	
	/**
	 * 
	 * @return String
	 * @param $field String
	 */
	static function field_to_name($field)
	{
		if (strpos($field, '.'))
		{
			list($model, $field) = explode('.', $field, 2);
			return "{$model}[{$field}]";
		}
		return $field;
	}
}

# little hack: auto bind controller's model (if any) and its relations.
if (isset($GLOBALS['self'], $GLOBALS['self']->{$GLOBALS['self']->model_class}))
{
	$model = $GLOBALS['self']->{$GLOBALS['self']->model_class};
	form::bind($GLOBALS['self']->model_class, $model);
	
	foreach($model->get_associations() as $assoc)
	{
		if (isset($model->$assoc)) {
			form::bind($assoc, $model->$assoc);
		}
	}
	unset($model);
}
?>